use crate::css::{Rule, Selector, SimpleSelector, Specificity, Stylesheet, Value};
use crate::dom::{ElementData, Node, NodeType};
use std::collections::HashMap;

pub type PropertyMap = HashMap<String, Value>;

#[derive(Debug)]
pub struct StyledNode<'a> {
  pub node: &'a Node,
  pub specified_values: PropertyMap,
  pub children: Vec<StyledNode<'a>>,
}

#[derive(PartialEq)]
pub enum Display {
  Inline,
  Block,
  None,
}

impl<'a> StyledNode<'a> {
  pub fn value(&self, name: &str) -> Option<Value> {
    self.specified_values.get(name).cloned()
  }

  pub fn lookup(&self, name: &str, fallback_name: &str, default: &Value) -> Value {
    self
      .value(name)
      .unwrap_or_else(|| self.value(fallback_name).unwrap_or_else(|| default.clone()))
  }

  pub fn display(&self) -> Display {
    match self.value("display") {
      Some(Value::Keyword(s)) => match &s[..] {
        "block" => Display::Block,
        "none" => Display::None,
        _ => Display::Inline,
      },
      _ => Display::Inline,
    }
  }
}

pub fn style_tree<'a>(root: &'a Node, stylesheet: &'a Stylesheet) -> StyledNode<'a> {
  StyledNode {
    node: root,
    specified_values: match root.node_type {
      NodeType::Element(ref elem) => specified_values(elem, stylesheet),
      NodeType::Text(_) => HashMap::new(),
    },
    children: root
      .children
      .iter()
      .map(|child| style_tree(child, stylesheet))
      .collect(),
  }
}

fn specified_values(elem: &ElementData, stylesheet: &Stylesheet) -> PropertyMap {
  let mut values = PropertyMap::new();
  let mut rules = matching_rules(elem, stylesheet);

  rules.sort_by(|&(a, _), &(b, _)| a.cmp(&b));
  for (_, rule) in rules {
    for declaration in &rule.declarations {
      values.insert(declaration.name.clone(), declaration.value.clone());
    }
  }
  values
}

type MatchedRule<'a> = (Specificity, &'a Rule);

fn matching_rules<'a>(elem: &ElementData, stylesheet: &'a Stylesheet) -> Vec<MatchedRule<'a>> {
  stylesheet
    .rules
    .iter()
    .filter_map(|rule| match_rule(elem, rule))
    .collect()
}

fn match_rule<'a>(elem: &ElementData, rule: &'a Rule) -> Option<MatchedRule<'a>> {
  rule
    .selectors
    .iter()
    .find(|selector| matches(elem, *selector))
    .map(|selector| (selector.specificity(), rule))
}

fn matches(elem: &ElementData, selector: &Selector) -> bool {
  match selector {
    Selector::Simple(simple_selector) => match_simple_selector(elem, simple_selector),
  }
}

fn match_simple_selector(elem: &ElementData, selector: &SimpleSelector) -> bool {
  if selector.tag_name.iter().any(|name| elem.tag_name != *name) {
    return false;
  }

  if selector.id.iter().any(|id| elem.id() != Some(id)) {
    return false;
  }

  let elem_classes = elem.classes();
  if selector
    .class
    .iter()
    .any(|class| !elem_classes.contains(&class[..]))
  {
    return false;
  }

  true
}

#[cfg(test)]
mod test {
  use super::*;
  use crate::css;
  use crate::dom::AttrMap;
  use crate::html;

  #[test]
  fn match_simple_selector_tag_name() {
    let elem = ElementData {
      tag_name: "div".to_string(),
      attributes: AttrMap::new(),
    };
    let simple_selector = SimpleSelector {
      tag_name: Some("div".to_string()),
      id: None,
      class: Vec::new(),
    };
    assert_eq!(match_simple_selector(&elem, &simple_selector), true);
  }

  #[test]
  fn match_simple_selector_id() {
    let mut attrs = AttrMap::new();
    attrs.insert("id".to_string(), "aa".to_string());
    let elem = ElementData {
      tag_name: "div".to_string(),
      attributes: attrs,
    };
    let simple_selector = SimpleSelector {
      tag_name: None,
      id: Some("aa".to_string()),
      class: Vec::new(),
    };
    assert_eq!(match_simple_selector(&elem, &simple_selector), true);
  }

  #[test]
  fn style_entry() {
    let node = html::parse("<html> <a href='http://qq.com'></a> <div id='aa'><span class='classa classb'>test</span></div></html>".to_string());
    let stylesheet =
      css::parse("#id {display:inline;} .classa {color: red;} a {font-size:1px;}".to_string());
    let tree = style_tree(&node, &stylesheet);
    assert_eq!(tree.children.len(), 2);
  }
}
